查看原文
其他

什么时候需要序列化和反序列化?

点击关注 👉 Java核心技术 2023-02-26

戳这里,加关注哦~

什么时候需要序列化和反序列化

简单的写一个 hello world 程序,用不到序列化和反序列化。写一个排序算法也用不到序列化和反序列化。但是当你想要将一个对象进行持久化写入文件,或者你想将一个对象从一个网络地址通过网络协议发送到另一个网络地址时,这时候就需要考虑序列化和反序列化了。另外如果你想对一个对象实例进行深度拷贝,也可以通过序列化和反序列化的方式进行。

什么是序列化和反序列化

Serialization - 序列化:可以看做是将一个对象转化为二进制流的过程
Deserialization - 反序列化:可以看做是将对象的二进制流重新读取转换成对象的过程

怎么实现序列化

只有实现了 Serializable 或 Externalizable 接口的类的对象才能被序列化,否则抛出异常。

Serializable 接口

Serializable 接口是 Java 提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用 Serializable 来实现的对象的序列化相当简单,只需要在类的生命中指定一个类似相面的标识即可自动实现默认的序列化过程。
这种方式是 Java 提供的一种序列化方式,过程非常简单,甚至有些开发人员都不需要声明 serialVersionUID 也可以完成这个过程,但 serialVersionUID 到底需不需要指定呢?
需要!Java API 既然提供了这个 serialVersionUID,那么它必定是有用的。这个 serialVersionUID 是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的 serialVersionUID 只有和当前类的 serialVersionUID 相同才能够正常地被反序列化。
serialVersionUID 的详细工作过程是这样的:序列化的时候系统会把当前类的 serialVersionUID 写入序列化的二进制文件中,当反序列化的时候系统会检测文件中的 serialVersionUID 是否和当前类的 serialVersionUID 一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;否则说明当前类和反序列化的类相比发生了某些变化,比如成员变量的数量、类型发生了变化,这个时候是无法正常反序列化的。
一般来说,我们应该手动指定 serialVersionUID 的值,比如 1L,也可以让 IDE 根据当前类的结构自动去生成它的 hash 值,这样序列化和反序列化时两者的 serialVersionUID 是相同的,因此可以正常进行反序列化操作。如果不手动指定 serialVersionUID 的值反序列化时当前类有些改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的 hash 值并把它赋值给 serialVersionUID,这个时候当前类的 serialVersionUID 就和反序列化数据中的 serialVersionUID 不一致,就会造成反序列化失败的结果。
所以,手动指定 serialVersionUID 可以在很大程度上避免反序列化过程的失败。比如当版本升级后,我们可能删除了某个成员变量也可能增加了一些新的成员变量,这个时候我们的反序列化过程依然能够成功,程序仍然能够最大限度地回复数据;相反,如果不指定 serialVersionUID 的话,程序会发生 Crash。
当然,我们还需要考虑一种情况,如果类结构发生了非常规改变,比如修改了类名,修改了成员变量的类型,这个时候尽管 serialVersionUID 验证通过了,但是反序列化过程仍然会失败,因为类的结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构的对象。
对于实现了这两个接口,具体序列化和反序列化的过程又分以下 3 种类情况:

情况 1:若类仅仅实现了 Serializable 接口

ObjectOutputStream 采用默认的序列化方式,对对象的非 transient 的实例变量进行序列化。ObjcetInputStream 采用默认的反序列化方式,对对象的非 transient 的实例变量进行反序列化。

情况 2:若类不仅实现了 Serializable 接口,并且还定义了 readObject(ObjectInputStream in) 和 writeObject(ObjectOutputSteam out)。

ObjectOutputStream 调用对象的 writeObject(ObjectOutputStream out) 的方法进行序列化。ObjectInputStream 会调用对象的 readObject(ObjectInputStream in) 的方法进行反序列化。

情况 3:若类实现了 Externalnalizable 接口,且类必须实现 readExternal(ObjectInput in) 和 writeExternal(ObjectOutput out) 方法。

ObjectOutputStream 调用对象的 writeExternal(ObjectOutput out)) 的方法进行序列化。ObjectInputStream 会调用对象的 readExternal(ObjectInput in) 的方法进行反序列化。
为了进一步说明,我们直接看 jdk 底层 ArrayList 的序列化和反序列化:
// 实现了Serializable接口,可以被序列化
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer.
     */
    // 实际元素被transient修饰,默认不会进行序列化
    private transient Object[] elementData;

    .....

    /**
     * Save the state of the <tt>ArrayList</tt> instance to a stream (that
     * is, serialize it).
     *
     * @serialData The length of the array backing the <tt>ArrayList</tt>
     *             instance is emitted (int), followed by all of its elements
     *             (each an <tt>Object</tt>) in the proper order.
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

        // Write out array length
        s.writeInt(elementData.length);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++)
            s.writeObject(elementData[i]);

    if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

    }

    /**
     * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

        // Read in array length and allocate array
        int arrayLength = s.readInt();
        Object[] a = elementData = new Object[arrayLength];

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++)
            a[i] = s.readObject();
    }
}
可以看到,初看之下 ArrayList 的实际存储元素不能被序列化。但实际上根据我们上面的第二条原则,知道因为其重写了 writeObject 和 readObject 方法, 而在方法的内部实现了对具体存储对象的序列化与反序列化。那么这两个方法究竟是在什么时候执行的呢?我们需要转到 ObjectOutputStream 这个对象上来:
/**
 * Serialization's descriptor for classes.  It contains the name and
 * serialVersionUID of the class.  The ObjectStreamClass for a specific class
 * loaded in this Java VM can be found/created using the lookup method. 16  */
// 在序列化对象之前会封装一个ObjectStreamClass对象
public class ObjectStreamClass implements Serializable  {
    /** class-defined writeObject method, or null if none */
    private Method writeObjectMethod;

     /**
     * Creates local class descriptor representing given class.
     */
    private ObjectStreamClass(final Class cl) { 36    
      ......
    if (serializable) {
        AccessController.doPrivileged(new PrivilegedAction() {
        public Object run() {
            if (isEnum) {
            suid = Long.valueOf(0);
            fields = NO_FIELDS;
            return null;
            }
            if (cl.isArray()) {
            fields = NO_FIELDS;
            return null;
            }

            suid = getDeclaredSUID(cl);
            try {
            fields = getSerialFields(cl);
            computeFieldOffsets();
            } catch (InvalidClassException e) {
            serializeEx = deserializeEx = e;
            fields = NO_FIELDS;
            }

            if (externalizable) {
            cons = getExternalizableConstructor(cl);
            } else {
            cons = getSerializableConstructor(cl);
                        // 其实就是writeObject方法
            writeObjectMethod = getPrivateMethod(cl, "writeObject", 
                new Class[] { ObjectOutputStream.class }, 
                Void.TYPE);
            readObjectMethod = getPrivateMethod(cl, "readObject", 
                new Class[] { ObjectInputStream.class }, 
                Void.TYPE);
            readObjectNoDataMethod = getPrivateMethod(
                cl, "readObjectNoData", null, Void.TYPE);
            hasWriteObjectData = (writeObjectMethod != null);
            }
            writeReplaceMethod = getInheritableMethod(
            cl, "writeReplace", null, Object.class);
            readResolveMethod = getInheritableMethod(
            cl, "readResolve", null, Object.class);
            return null;
        }
        });
    } else {
        suid = Long.valueOf(0);
        fields = NO_FIELDS;
    }

     .......107     }

    /**
     * Returns non-static private method with given signature defined by given
     * class, or null if none found.  Access checks are disabled on the
     * returned method (if any).
     */
    private static Method getPrivateMethod(Class cl, String name, 
                       Class[] argTypes,
                       Class returnType)
    {
    try {
        Method meth = cl.getDeclaredMethod(name, argTypes);
        meth.setAccessible(true);
        int mods = meth.getModifiers();
        return ((meth.getReturnType() == returnType) &&
            ((mods & Modifier.STATIC) == 0) &&
            ((mods & Modifier.PRIVATE) != 0)) ? meth : null;
    } catch (NoSuchMethodException ex) {
        return null;
    }
    }

     /**
     * Returns true if represented class is serializable (but not
     * externalizable) and defines a conformant writeObject method.  Otherwise,
     * returns false.
     */
    boolean hasWriteObjectMethod() {
    return (writeObjectMethod != null);
    }
}

public class ObjectOutputStream
    extends OutputStream implements ObjectOutput, ObjectStreamConstants
{
    /**
     * Magic number that is written to the stream header.
     */
    final static short STREAM_MAGIC = (short)0xaced;

    /**
     * Version number that is written to the stream header.
     */
    final static short STREAM_VERSION = 5;

    public ObjectOutputStream(OutputStream out) throws IOException {
    verifySubclass();
    bout = new BlockDataOutputStream(out);
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    enableOverride = false;
        // 写入头信息
    writeStreamHeader();
    bout.setBlockDataMode(true);
        if (extendedDebugInfo) {
        debugInfoStack = new DebugTraceInfoStack();
    } else {
        debugInfoStack = null;
        }   
    }

     protected void writeStreamHeader() throws IOException {
    bout.writeShort(STREAM_MAGIC);
    bout.writeShort(STREAM_VERSION);
    }

    /**
     * Write the specified object to the ObjectOutputStream.  The class of the
     * object, the signature of the class, and the values of the non-transient
     * and non-static fields of the class and all of its supertypes are
     * written.  Default serialization for a class can be overridden using the
     * writeObject and the readObject methods.  Objects referenced by this
     * object are written transitively so that a complete equivalent graph of
     * objects can be reconstructed by an ObjectInputStream.196      */
    public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
        writeFatalException(ex);
        }
        throw ex;
    }
    }

     /**
     * Underlying writeObject/writeUnshared implementation.
     */
    private void writeObject0(Object obj, boolean unshared) 
    throws IOException 
    {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        // handle previously written and non-replaceable objects
       ......
        // check for replacement object
        ......241      261 
        // if object replaced, run through original checks a second time
       ......279 
        // remaining cases
        if (obj instanceof String) {
        writeString((String) obj, unshared);
        } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
        writeEnum((Enum) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
                // 如果不是特殊对象类型,最终会调用该方法
        writeOrdinaryObject(obj, desc, unshared);
        } else {
        if (extendedDebugInfo) {
            throw new NotSerializableException(
            cl.getName() + "\n" + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }    
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
    }   

    private void writeOrdinaryObject(Object obj, 
                     ObjectStreamClass desc, 
                     boolean unshared) 
    throws IOException 
    {
        if (extendedDebugInfo) {
        debugInfoStack.push(
        (depth == 1 ? "root " : "") + "object (class \"" + 
        obj.getClass().getName() + "\", " + obj.toString() + ")");
        }
        try {
        desc.checkSerialize();

        bout.writeByte(TC_OBJECT);
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
        writeExternalData((Externalizable) obj);
        } else {
                // 一般情况下会调用该方法
        writeSerialData(obj, desc);
        }
    } finally {
            if (extendedDebugInfo) {
        debugInfoStack.pop();
        }  
        }
    }            

   /**
     * Writes instance data for each serializable class of given object, from
     * superclass to subclass.
     */
    private void writeSerialData(Object obj, ObjectStreamClass desc) 
    throws IOException 
    {
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
            // 如果重写了序列化的方法writeObject,则调用对应的方法进行写入,其实就是ObjectStreamClass 中的对应方法,可以得出序列化的第2条规则
        if (slotDesc.hasWriteObjectMethod()) {
        PutFieldImpl oldPut = curPut;
        curPut = null;

        if (extendedDebugInfo) {
            debugInfoStack.push(
            "custom writeObject data (class \"" + 
            slotDesc.getName() + "\")");
        }

                SerialCallbackContext oldContext = curContext;
        try {
                    curContext = new SerialCallbackContext(obj, slotDesc);

            bout.setBlockDataMode(true);
            slotDesc.invokeWriteObject(obj, this);
            bout.setBlockDataMode(false);
            bout.writeByte(TC_ENDBLOCKDATA);
        } finally {
                    curContext.setUsed();
                    curContext = oldContext;

            if (extendedDebugInfo) {
            debugInfoStack.pop();
            }    
        } 

        curPut = oldPut;
        } else {
                // 未重写调用默认的方法
        defaultWriteFields(obj, slotDesc);
        }
    }
}
以上代码就是分析序列化情况 2 的实现,反序列化也可以同样跟踪发现,这里不再重复。

总结序列化反序列化的注意点

  1. 父类实现了序列化,则子类自动实现了序列化,即子类不需要显式实现 Serializable 接口, 子类构造时会递归调用父类构造。
  2. 当父类没有实现序列化,而子类需要实现时,子类需要显式实现 Serializable 接口,并且父类中需要有无参的构造函数。
  3. 序列化只对对象的属性进行保存,而不会保存其方法。
  4. 当类中的实例变量引用了其他对象,那么在对该类进行序列化时,引用的对象也会被序列化(需要这个引用的对象也实现 Serializable 接口,否则会出现 java.io.NotSerializableException)。
  5. 静态成员变量属于类不属于对象,所以不参与序列化过程
  6. 用 transient 关键字标记的成员变量不参与序列化过程
作者:那啥快看
来源:cnblogs.com/shamo89/p/8483345.html

最后给大家送下福利,大家可以关注Java核心技术公众号,在后台回复 “福利”可以获取一份我整理的最新Java面试题资料。

最近好文分享

1、Java 顶级项目权威排名,结果出乎意料!

2、10 个最好用的重构小技巧排行榜

3、连 CopyOnWriteArrayList 都没听过!

4、IntelliJ IDEA 神级超级牛逼插件推荐

5、Spring Boot 中的 RestTemplate 不好用?

6、巧用枚举来干掉 if-else,代码更优雅!

……

更多请扫码关注 • Java核心技术

一个分享Java核心技术干货的公众号


点击阅读原文获取面试题~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存